package net.kazed.nextaction.database;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.ProviderException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.kazed.nextaction.R;
import net.kazed.nextaction.database.GtdContext.Column;
import net.kazed.nextaction.database.serialize.BackupFile;
import net.kazed.nextaction.database.serialize.BackupFileSummary;
import net.kazed.nextaction.database.serialize.BackupSummaryCursor;
import net.kazed.nextaction.database.serialize.BackupSummaryExternalCursor;
import net.kazed.nextaction.help.Help;
import net.kazed.nextaction.process.StartDateProcessor;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.UriMatcher;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Environment;
import android.provider.Contacts;
import android.text.TextUtils;
import android.util.Log;

/**
 * Provides access to database for all context and task related data.
 */
public class GtdProvider extends ContentProvider {
    
    public static final String BACKUP_SAVE_TO_SD_CARD = "saveToSdCard";
    public static final String BACKUP_IMPORT_FROM_SD_CARD = "importFromSdCard";
    public static final String DATABASE_NAME = "nextaction.db";

    private static final String TAG = "GtdProvider";
    private static Map<String, String> sTaskContactListProjectMap;
	
    private static final int TASKS = 1;
    private static final int TASK_ID = 2;
    private static final int TOP_TASKS = 3;
    private static final int INBOX_TASKS = 4;
    private static final int TASK_AND_SUB_TASKS = 5;
    private static final int TASK_CONTACTS = 10;
    private static final int TASK_ATTACHMENT = 11;
    private static final int TASK_ATTACHMENTS_BY_TASK = 12;
    private static final int TASK_TIMER = 13;
    private static final int PROJECTS = 20;
    private static final int HELP_PATH = 50;
    
    private static final int CONTEXTS = 101;
    private static final int CONTEXT_ID = 102;
    private static final int CONTEXT_TASKS = 103;
    private static final int TASKS_COMPLETED = 104;

    private static final int BACKUP_FILE = 201;
    private static final int BACKUP_FILE_ITEM = 202;
    private static final int BACKUP_SUMMARY = 203;
    private static final int BACKUP_SUMMARY_ITEM = 204;
    private static final int BACKUP_FILE_EXTERNAL = 205;
    private static final int BACKUP_FILE_EXTERNAL_ITEM = 206;
    
    private static final UriMatcher URI_MATCHER;

    private DatabaseHelper dbHelper;
    
    @Override
    public boolean onCreate() {
    	Log.i(TAG, "+onCreate");
    	dbHelper = new DatabaseHelper(getContext());
        return true;
    }
    
    @Override
    public String getType(Uri uri) {
       String type = null;
       int match = URI_MATCHER.match(uri);
        switch (match) {
        case TASKS:
        case INBOX_TASKS:
           type = Task.CONTENT_TYPE;
           break;
        case TASK_ID:
           type = Task.CONTENT_ITEM_TYPE;
           break;
        case TASK_CONTACTS:
           type = Contacts.People.CONTENT_TYPE;
           break;
        case TASK_ATTACHMENT:
            type = TaskAttachment.CONTENT_TYPE;
            break;
        case TASK_AND_SUB_TASKS:
            type = Task.ContentItemType.TASK_AND_SUB_TASKS;
            break;
        case TASK_TIMER:
           type = Task.CONTENT_TIMER_ITEM_TYPE;
           break;
        case CONTEXTS:
           type = GtdContext.CONTENT_TYPE;
           break;
        case CONTEXT_ID:
           type = GtdContext.CONTENT_ITEM_TYPE;
           break;
        case PROJECTS:
            type = Project.CONTENT_TYPE;
            break;
        case BACKUP_SUMMARY:
            type = BackupFileSummary.CONTENT_LIST_TYPE;
            break;
        case BACKUP_SUMMARY_ITEM:
            type = BackupFileSummary.CONTENT_ITEM_TYPE;
            break;
        case BACKUP_FILE:
            type = BackupFile.CONTENT_LIST_TYPE;
            break;
        case BACKUP_FILE_ITEM:
            type = BackupFile.CONTENT_ITEM_TYPE;
            break;
        case BACKUP_FILE_EXTERNAL:
            type = BackupFile.CONTENT_EXTERNAL_LIST_TYPE;
            break;
        case HELP_PATH:
           type = Help.CONTENT_ITEM_TYPE;
           break;
//        case PROJECTS:
//           type = Project.CONTENT_TYPE;
//           break;
//        case PROJECT_ID:
//           type = Project.CONTENT_ITEM_TYPE;
//           break;
        default:
            throw new IllegalArgumentException("Unknown Uri " + uri);
        }
        return type;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sort) {
        Cursor cursor = null;
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        SQLiteDatabase db = dbHelper.getReadableDatabase();

        switch (URI_MATCHER.match(uri)) {
        case TASKS:
            qb.setTables(DatabaseHelper.TABLE_TASK);
            qb.setProjectionMap(DatabaseHelper.TASK_LIST_PROJECT_MAP);
            cursor = executeQuery(db, qb, uri, projection, selection, selectionArgs, sort, Task.DEFAULT_SORT_ORDER);
            break;
        case TASK_ID:
            qb.setTables(DatabaseHelper.TABLE_TASK);
            qb.setProjectionMap(DatabaseHelper.TASK_LIST_PROJECT_MAP);
            qb.appendWhere(DatabaseHelper.TABLE_TASK + "._id=" + uri.getPathSegments().get(1));
            cursor = executeQuery(db, qb, uri, projection, selection, selectionArgs, sort, Task.DEFAULT_SORT_ORDER);
            break;
        case TASK_CONTACTS:
            qb.setTables(DatabaseHelper.TABLE_TASK_CONTACT);
            qb.setProjectionMap(sTaskContactListProjectMap);
            qb.appendWhere(DatabaseHelper.TABLE_TASK_CONTACT + ".taskId=" + uri.getPathSegments().get(1));
            cursor = executeQuery(db, qb, uri, projection, selection, selectionArgs, sort, TaskContact.DEFAULT_SORT_ORDER);
            break;
        case TASK_ATTACHMENT:
            qb.setTables(DatabaseHelper.TABLE_TASK_ATTACHMENT);
            qb.setProjectionMap(DatabaseHelper.TASK_ATTACHMENT_PROJECT_MAP);
            cursor = executeQuery(db, qb, uri, projection, selection, selectionArgs, sort, TaskAttachment.DEFAULT_SORT_ORDER);
            break;
        case TASK_ATTACHMENTS_BY_TASK:
            qb.setTables(DatabaseHelper.TABLE_TASK_ATTACHMENT);
            qb.setProjectionMap(DatabaseHelper.TASK_ATTACHMENT_PROJECT_MAP);
            qb.appendWhere(DatabaseHelper.TABLE_TASK_ATTACHMENT + ".taskId=" + uri.getPathSegments().get(1));
            cursor = executeQuery(db, qb, uri, projection, selection, selectionArgs, sort, TaskAttachment.DEFAULT_SORT_ORDER);
            break;
        case PROJECTS:
            qb.setTables(DatabaseHelper.TABLE_TASK);
            qb.appendWhere(Task.Column.TYPE + " = " + TaskStatus.PROJECT.getType());
            qb.setProjectionMap(DatabaseHelper.TASK_LIST_PROJECT_MAP);
            cursor = executeQuery(db, qb, uri, projection, selection, selectionArgs, sort, Task.DEFAULT_SORT_ORDER);
            break;
        case CONTEXTS:
            qb.setTables(DatabaseHelper.TABLE_CONTEXT);
            qb.setProjectionMap(DatabaseHelper.CONTEXT_LIST_PROJECT_MAP);
            cursor = executeQuery(db, qb, uri, projection, selection, selectionArgs, sort, GtdContext.DEFAULT_SORT_ORDER);
            break;
        case CONTEXT_ID:
            qb.setTables(DatabaseHelper.TABLE_CONTEXT);
            qb.appendWhere("_id=" + uri.getPathSegments().get(1));
            cursor = executeQuery(db, qb, uri, projection, selection, selectionArgs, sort, GtdContext.DEFAULT_SORT_ORDER);
            break;
        case CONTEXT_TASKS:
        	cursor = db.rawQuery("select c._id, count(*) count from context c, task t where t.contextId = c._id group by c._id" , null);
        	break;
        case BACKUP_SUMMARY:
            cursor = new BackupSummaryCursor(getContext(), false);
            break;
        case BACKUP_SUMMARY_ITEM:
            cursor = new BackupSummaryCursor(getContext(), uri, true);
            break;
        case BACKUP_FILE_EXTERNAL:
            cursor = new BackupSummaryExternalCursor(getContext());
            break;
        default:
            throw new IllegalArgumentException("Unknown URL " + uri);
        }

        cursor.setNotificationUri(getContext().getContentResolver(), uri);
        return cursor;
    }
    
    private Cursor executeQuery(SQLiteDatabase db, SQLiteQueryBuilder qb, Uri uri, String[] projection, String selection,
                    String[] selectionArgs, String orderBy, String defaultOrderBy) {
        String orderByClause = orderBy;
        if (TextUtils.isEmpty(orderByClause)) {
            orderByClause = defaultOrderBy;
        }
        if (Log.isLoggable(TAG, Log.INFO)) {
            Log.i(TAG, "Executing " + selection + " with args " + Arrays.toString(selectionArgs) +
                    " ORDER BY " + orderBy);
            Log.i(TAG, "Query builder: " + qb.toString());
        }
        Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderByClause);
        c.setNotificationUri(getContext().getContentResolver(), uri);
        return c;
    }
    
    @Override
    public Uri insert(Uri url, ContentValues initialValues) {
        Uri insertedUri = null;
        long rowID;
        ContentValues values;
        Resources r;
        if (initialValues != null) {
            values = new ContentValues(initialValues);
        } else {
            values = new ContentValues();
        }

        SQLiteDatabase db = dbHelper.getWritableDatabase();

        switch (URI_MATCHER.match(url)) {
        case TASKS:
        case INBOX_TASKS:
            Long now = Long.valueOf(System.currentTimeMillis());
            r = getContext().getResources();

            // Make sure that the fields are all set
            if (! values.containsKey(Task.Column.CREATED_DATE)) {
                values.put(Task.Column.CREATED_DATE, now);
            }
            if (! values.containsKey(Task.Column.MODIFIED_DATE)) {
                values.put(Task.Column.MODIFIED_DATE, now);
            }
            if (! values.containsKey(Task.Column.DESCRIPTION)) {
                values.put(Task.Column.DESCRIPTION, r.getString(R.string.initial_text));
            }
            if (! values.containsKey(Task.Column.DETAILS)) {
                values.put(Task.Column.DETAILS, "");
            }
            if (! values.containsKey(Task.Column.DISPLAY_ORDER)) {
            	values.put(Task.Column.DISPLAY_ORDER, 0);
            }
            if (! values.containsKey(Task.Column.COMPLETE)) {
            	values.put(Task.Column.COMPLETE, 0);
            }

            rowID = db.insert(DatabaseHelper.TABLE_TASK, DatabaseHelper.TASK_LIST_PROJECT_MAP.get(Task.Column.DESCRIPTION), values);
            if (rowID > 0) {
                changeContextTask(db, (int) rowID, 1);
                insertedUri = ContentUris.withAppendedId(Task.CONTENT_URI, rowID);
                updateContextNumberOfTasks(db, values.getAsInteger(Task.Column.CONTEXT_ID), 0);
                SqlTemplate template = new SqlTemplate(db);
                updateTaskNumberOfTasks(db, template, values.getAsInteger(Task.Column.PARENT_TASK_ID));
                getContext().getContentResolver().notifyChange(insertedUri, null);
            } else {
                getContext().getContentResolver().notifyChange(Task.CONTENT_URI, null);
            }
            checkDeferredTasks(db);
        	break;
        	
        case TASK_CONTACTS:
        	String taskId = url.getPathSegments().get(1);
            values.put(TaskContact.TASK_ID, taskId);
            rowID = db.insert(DatabaseHelper.TABLE_TASK_CONTACT, sTaskContactListProjectMap.get(TaskContact.TASK_ID), values);
            if (rowID > 0) {
                insertedUri = ContentUris.withAppendedId(TaskContact.CONTENT_URI, rowID);
                getContext().getContentResolver().notifyChange(insertedUri, null);
            }
    		break;
        	
        case TASK_ATTACHMENT:
            rowID = db.insert(DatabaseHelper.TABLE_TASK_ATTACHMENT,
                            DatabaseHelper.TASK_ATTACHMENT_PROJECT_MAP.get(TaskAttachment.Column.TASK_ID), values);
            if (rowID > 0) {
                insertedUri = ContentUris.withAppendedId(TaskAttachment.CONTENT_URI, rowID);
                getContext().getContentResolver().notifyChange(insertedUri, null);
            }
            break;
            
        	
//        case PROJECTS:
//            r = android.content.res.Resources.getSystem();
//            if (values.containsKey(Project.NAME) == false) {
//                values.put(Project.NAME, r.getString(android.R.string.untitled));
//            }
//            
//            rowID = db.insert(cProjectTableName, sProjectListProjectMap.get(Project.NAME), values);
//            if (rowID > 0) {
//                Uri uri = ContentUris.withAppendedId(Project.CONTENT_URI, rowID);
//                getContext().getContentResolver().notifyChange(uri, null);
//                return uri;
//            }
//        	break;
        	
        case CONTEXTS:
            r = Resources.getSystem();
            if (values.containsKey(GtdContext.NAME) == false) {
                values.put(GtdContext.NAME, r.getString(android.R.string.untitled));
            }
            
            rowID = db.insert(DatabaseHelper.TABLE_CONTEXT, GtdContext.NAME, values);
            if (rowID > 0) {
                insertedUri = ContentUris.withAppendedId(GtdContext.CONTENT_URI, rowID);
                getContext().getContentResolver().notifyChange(insertedUri, null);
            } else {
                getContext().getContentResolver().notifyChange(GtdContext.CONTENT_URI, null);
            }
        	break;
        	    	
        case BACKUP_FILE:
            Date created = new Date();
            String fileName = Long.toString(created.getTime()) + ".nab";
            BackupFile backupFile = new BackupFile(fileName);
            backupFile.createBackup(getContext(), db);
            insertedUri = ContentUris.withAppendedId(BackupFile.CONTENT_URI, created.getTime());
            break;
                    
    	default:
            throw new IllegalArgumentException("Unknown URL " + url);
        }

        return insertedUri;

//        throw new SQLException("Failed to insert row into " + url);
    }

    @Override
    public int delete(Uri uri, String where, String[] whereArgs) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
    	
        int count = 0;
        String segment;
        switch (URI_MATCHER.match(uri)) {
        case TASKS:
        case INBOX_TASKS:
            count = db.delete(DatabaseHelper.TABLE_TASK, where, whereArgs);
            getContext().getContentResolver().notifyChange(Task.CONTENT_URI, null);
            break;
        case TASK_ID: {
                Integer taskId = Integer.parseInt(uri.getPathSegments().get(1));
                changeContextTask(db, taskId, -1);
                SqlTemplate template = new SqlTemplate(db);
                Integer parentTaskId = getParentTaskId(template, taskId);
                int topTaskId = getTopParentTaskId(template, taskId);
                count = db.delete(DatabaseHelper.TABLE_TASK, "_id = " + taskId.toString()
                                + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
                if (parentTaskId != null) {
                    ContentValues values = new ContentValues();
                    values.putNull(Task.Column.PARENT_TASK_ID);
                    db.update(DatabaseHelper.TABLE_TASK, values, Task.Column.PARENT_TASK_ID + " = ?",
                                    new String[] {parentTaskId.toString()});
                }
                detachTasksFromParent(db, taskId);
                getContext().getContentResolver().notifyChange(uri, null);
                updateNumberOfSubTasksOfParentTask(db, topTaskId);
            }
            break;
        case TASK_AND_SUB_TASKS: {
                segment = uri.getPathSegments().get(1);
                changeContextTask(db, Integer.parseInt(segment), -1);
                deleteSubTasks(db, getContext().getContentResolver(), Integer.parseInt(segment));
                count = db.delete(DatabaseHelper.TABLE_TASK, "_id = " + segment
                                + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
                getContext().getContentResolver().notifyChange(uri, null);
            }
            break;
        case TASK_CONTACTS:
        	long id = ContentUris.parseId(uri);
            Log.d(TAG, "Deleting task contacts for task " + id);
            count = db
                    .delete(DatabaseHelper.TABLE_TASK_CONTACT,
                    		(id > 0 ? "taskId = " + id : "")
                            + (!TextUtils.isEmpty(where) ? " AND (" + where
                                    + ')' : ""), whereArgs);
            Log.d(TAG, "Deleted " + count + " task contacts");
    		break;
        case TASKS_COMPLETED: {
                detachCompletedTasksFromParent(db);
                String whereClause = Task.Column.COMPLETE + " = ?";
                String[] selectionArgs = new String[] {"1"};
                count = db.delete(DatabaseHelper.TABLE_TASK, whereClause, selectionArgs);
                Log.d(TAG, "Deleted " + count + " tasks");
                dbHelper.updateNumberOfTasksInAllContexts(db);
                dbHelper.updateNumberOfSubTasksInAllTasks(db);
                getContext().getContentResolver().notifyChange(Task.CONTENT_URI, null);
            }
            break;
        case TASK_ATTACHMENT:
            long taskId = ContentUris.parseId(uri);
            if (taskId > 0) {
                Log.d(TAG, "Deleting task attachments for task " + taskId);
                count = db.delete(DatabaseHelper.TABLE_TASK_ATTACHMENT, "taskId = " + taskId
                                + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
            } else {
                count = db.delete(DatabaseHelper.TABLE_TASK_ATTACHMENT, where, whereArgs);
            }
            Log.d(TAG, "Deleted " + count + " task attachments");
            break;
        case CONTEXTS:
            count = db.delete(DatabaseHelper.TABLE_CONTEXT, where, whereArgs);
            getContext().getContentResolver().notifyChange(GtdContext.CONTENT_URI, null);
            break;
        case CONTEXT_ID:
            segment = uri.getPathSegments().get(1);
            count = db
                    .delete(DatabaseHelper.TABLE_CONTEXT, "_id="
                            + segment
                            + (!TextUtils.isEmpty(where) ? " AND (" + where
                                    + ')' : ""), whereArgs);
            ContentValues taskValues = new ContentValues();
            taskValues.putNull(Task.Column.CONTEXT_ID);
            int count1 = db.update(DatabaseHelper.TABLE_TASK, taskValues, Task.Column.CONTEXT_ID + "=" + segment, null);
            Log.i(TAG, "updated count1: " + count1);
            ContentValues contextValues = new ContentValues();
            contextValues.putNull(Column.PARENT_CONTEXT_ID);
            int count2 = db.update(DatabaseHelper.TABLE_CONTEXT, contextValues, Column.PARENT_CONTEXT_ID + "=" + segment, null);
            Log.i(TAG, "updated count2: " + count2);
            getContext().getContentResolver().notifyChange(GtdContext.CONTENT_URI, null);
            break;
        case BACKUP_FILE_ITEM:
            String backupId = uri.getLastPathSegment();
            String fileName = backupId + ".nab";
            getContext().deleteFile(fileName);
            break;
//        case PROJECTS:
//            count = db.delete(cProjectTableName, where, whereArgs);
//            break;
//        case PROJECT_ID:
//            segment = uri.getPathSegments().get(1);
//            count = db
//                    .delete(cProjectTableName, "_id="
//                            + segment
//                            + (!TextUtils.isEmpty(where) ? " AND (" + where
//                                    + ')' : ""), whereArgs);
//            break;

        default:
            throw new IllegalArgumentException("Unknown uri " + uri);
        }

        return count;
    }

    @Override
    public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int count = 0;
        String segment;
        switch (URI_MATCHER.match(uri)) {
        case TASKS:
        case INBOX_TASKS:
            count = db.update(DatabaseHelper.TABLE_TASK, values, where, whereArgs);
            break;
        case TASK_ID: {
                count = updateTask(uri, values, where, whereArgs, db, Integer.parseInt(uri.getPathSegments().get(1)));
            }
            break;
        case TASK_AND_SUB_TASKS: {
                count = updateSubTasks(values, db, Integer.parseInt(uri.getPathSegments().get(1)));
                count += updateTask(uri, values, where, whereArgs, db, Integer.parseInt(uri.getPathSegments().get(1)));
            }
            break;
        case TASK_ATTACHMENT:
            count = genericUpdate(db, DatabaseHelper.TABLE_TASK_ATTACHMENT, uri, values, where, whereArgs);
            break;
        case CONTEXTS:
            count = db.update(DatabaseHelper.TABLE_CONTEXT, values, where, whereArgs);
            break;
        case CONTEXT_ID:
            segment = uri.getPathSegments().get(1);
            count = db
                    .update(DatabaseHelper.TABLE_CONTEXT, values, "_id="
                            + segment
                            + (!TextUtils.isEmpty(where) ? " AND (" + where
                                    + ')' : ""), whereArgs);
            break;
        case BACKUP_FILE_ITEM:
            String backupId = uri.getLastPathSegment();
            String fileName = backupId + ".nab";
            db.beginTransaction();
            try {
                if ("merge".equals(where)) {
                    BackupFile backupFile = new BackupFile(fileName);
                    backupFile.restoreMergeBackup(getContext());
                } else if ("replace".equals(where)) {
                    db.delete(DatabaseHelper.TABLE_CONTEXT, null, null);
                    db.delete(DatabaseHelper.TABLE_TASK, null, null);
                    BackupFile backupFile = new BackupFile(fileName);
                    backupFile.restoreReplaceBackup(getContext());
                } else if (BACKUP_SAVE_TO_SD_CARD.equals(where)) {
                    // copy fileName to SD card
                    File exportedFile = new File(Environment.getExternalStorageDirectory(), fileName);
                    FileOutputStream output = null;
                    FileInputStream input = null;
                    try {
                        output = new FileOutputStream(exportedFile);
                        input = getContext().openFileInput(fileName);
                        boolean copying = true;
                        byte buffer[] = new byte[4096];
                        while(copying) {
                            int length = input.read(buffer);
                            if (length < 0) {
                                copying = false;
                            } else {
                                output.write(buffer, 0, length);
                            }
                        }
                    } catch (IOException e) {
                        throw new ProviderException("failed to copy backup file to SD card", e);
                    } finally {
                        if (output != null) {
                            try { output.close(); } catch (IOException e) {}
                        }
                        if (input != null) {
                            try { input.close(); } catch (IOException e) {}
                        }
                    }
                }
                db.setTransactionSuccessful();
            } finally {
                db.endTransaction();
            }
            getContext().getContentResolver().notifyChange(GtdContext.CONTENT_URI, null);
            getContext().getContentResolver().notifyChange(Task.CONTENT_URI, null);
            break;
        case BACKUP_FILE_EXTERNAL_ITEM:
            String backupExternalId = uri.getLastPathSegment();
            String fileNameExternal = backupExternalId + ".nab";
            if (BACKUP_IMPORT_FROM_SD_CARD.equals(where)) {
                // copy fileName from SD card
                File externalFile = new File(Environment.getExternalStorageDirectory(), fileNameExternal);
                FileInputStream input = null;
                FileOutputStream output = null;
                try {
                    input =  new FileInputStream(externalFile);
                    output = getContext().openFileOutput(fileNameExternal, Context.MODE_WORLD_WRITEABLE);
                    boolean copying = true;
                    byte buffer[] = new byte[4096];
                    while(copying) {
                        int length = input.read(buffer);
                        if (length < 0) {
                            copying = false;
                        } else {
                            output.write(buffer, 0, length);
                        }
                    }
                } catch (IOException e) {
                    throw new ProviderException("failed to copy backup file from SD card", e);
                } finally {
                    if (output != null) {
                        try { output.close(); } catch (IOException e) {}
                    }
                    if (input != null) {
                        try { input.close(); } catch (IOException e) {}
                    }
                }
            }
            getContext().getContentResolver().notifyChange(GtdContext.CONTENT_URI, null);
            getContext().getContentResolver().notifyChange(Task.CONTENT_URI, null);
            break;
//        case PROJECTS:
//            count = db.update(cProjectTableName, values, where, whereArgs);
//            break;
//        case PROJECT_ID:
//            segment = uri.getPathSegments().get(1);
//            count = db
//                    .update(cProjectTableName, values, "_id="
//                            + segment
//                            + (!TextUtils.isEmpty(where) ? " AND (" + where
//                                    + ')' : ""), whereArgs);
//            break;

        default:
            throw new IllegalArgumentException("Unknown URL " + uri);
        }

        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }
    
    /**
     * Detach all sub tasks from all completed tasks.
     * @param db Database.
     */
    private void detachCompletedTasksFromParent(SQLiteDatabase db) {
        SqlTemplate template = new SqlTemplate(db);
        List<Integer> taskIds = template.queryList(DatabaseHelper.TABLE_TASK, DatabaseHelper.TASK_LIST_PROJECT_MAP,
                        new String[] {Task.Column.ID},
                        Task.Column.COMPLETE + " = ?", new String[] {Task.COMPLETED_TRUE}, 
                        new SqlTemplate.QueryHandler<Integer>() {
            @Override
            public Integer execute(Cursor cursor) {
                Integer taskId = null;
                if (!cursor.isNull(0)) {
                    taskId = cursor.getInt(0);
                }
                return taskId; 
            }
        });
        ContentValues values = new ContentValues();
        values.putNull(Task.Column.PARENT_TASK_ID);
        for (Integer taskId : taskIds) {
            db.update(DatabaseHelper.TABLE_TASK, values, Task.Column.PARENT_TASK_ID + " = ?",
                            new String[] {Integer.toString(taskId)});
        }

    }

    /**
     * Detach all tasks from their parent.
     * @param db Database.
     * @param parentTaskId Parent task ID.
     */
    private void detachTasksFromParent(SQLiteDatabase db, Integer parentTaskId) {
        ContentValues values = new ContentValues();
        values.putNull(Task.Column.PARENT_TASK_ID);
        db.update(DatabaseHelper.TABLE_TASK, values, Task.Column.PARENT_TASK_ID + " = ?",
                        new String[] {Integer.toString(parentTaskId)});
    }

    private int updateTask(Uri uri, ContentValues values, String where, String[] whereArgs, SQLiteDatabase db, int taskId) {
        changeContextTask(db, taskId, -1);
        SqlTemplate template = new SqlTemplate(db);
        Integer oldParentTaskId = getParentTaskId(template, taskId);
        int count = genericUpdate(db, DatabaseHelper.TABLE_TASK, uri, values, where, whereArgs);
        changeContextTask(db, taskId, 1);
        checkDeferredTasks(db);
        Integer newParentTaskId = getParentTaskId(template, taskId);
        if (oldParentTaskId != newParentTaskId) {
            if (oldParentTaskId != null) {
                updateNumberOfSubTasksOfParentTask(db, getTopParentTaskId(template, oldParentTaskId));
            }
            if (newParentTaskId != null) {
                updateNumberOfSubTasksOfParentTask(db, getTopParentTaskId(template, newParentTaskId));
            }
        }
        return count;
    }

    private int updateSubTasks(ContentValues values, SQLiteDatabase db, int taskId) {
        int count = 0;
        Cursor taskCursor = null;
        try {
            SQLiteQueryBuilder taskQuery = new SQLiteQueryBuilder();
            taskQuery.setTables(DatabaseHelper.TABLE_TASK);
            taskQuery.setProjectionMap(DatabaseHelper.TASK_LIST_PROJECT_MAP);
            taskCursor = taskQuery.query(db, new String[] {Task.Column.ID}, Task.Column.PARENT_TASK_ID + " = ?",
                            new String[] {Integer.toString(taskId)}, null, null, null);
            for (taskCursor.moveToFirst(); !taskCursor.isAfterLast(); taskCursor.moveToNext()) {
                int subTaskId = taskCursor.getInt(0);
                count += db.update(DatabaseHelper.TABLE_TASK, values, GtdContext.ID + " = ?",
                                new String[] {Integer.toString(subTaskId)});
                count += updateSubTasks(values, db, subTaskId);
            }
        } finally {
            if (taskCursor != null) {
                taskCursor.close();
            }
        }
        return count;
    }

    /**
     * Update a generic update.
     * @param db Database.
     * @param uri Database item URI.
     * @param values Updated values.
     * @param where Where clause.
     * @param whereArgs Where clause arguments.
     * @return Number of rows updated.
     */
    private int genericUpdate(SQLiteDatabase db, String tableName, Uri uri, ContentValues values, String where, String[] whereArgs) {
        String segment = uri.getPathSegments().get(1);
        int count = db.update(tableName, values, "_id=" + segment
                        + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
        return count;
    }
    
    /**
     * Check if the database contains deferred tasks with a start date.
     * @param db Database.
     */
    private void checkDeferredTasks(SQLiteDatabase db) {
        Cursor cursor = null;
        try {
            SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
            qb.setTables(DatabaseHelper.TABLE_TASK);
            qb.setProjectionMap(DatabaseHelper.TASK_LIST_PROJECT_MAP);
            cursor = executeQuery(db, qb, Task.CONTENT_URI, Task.ID_PROJECTION.getFieldNames(),
                            Task.Column.START_DATE + " is not null and " + Task.Column.STATUS + " = " + TaskStatus.IDEA.getType(),
                            null, null, null);
            if (cursor.getCount() > 0) {
                Context context = getContext();
                AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
                Intent processIntent = new Intent(context, StartDateProcessor.class);
                PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0, processIntent, 0);
                Calendar calendar = GregorianCalendar.getInstance();
                calendar.set(Calendar.HOUR_OF_DAY, 0);
                calendar.set(Calendar.MINUTE, 10);
                calendar.roll(Calendar.DAY_OF_YEAR, 1);
//                calendar.roll(Calendar.MINUTE, 1);
                alarmManager.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarmIntent);
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }
    
    /**
     * Update the number of tasks count of a context.
     * @param db Database.
     * @param contextId ID of context.
     */
    private void updateContextNumberOfTasks(SQLiteDatabase db, Integer contextId, int numberOfTasksAdjustment) {
        if (contextId != null) {
            Cursor taskCursor = null;
            try {
                SQLiteQueryBuilder taskQuery = new SQLiteQueryBuilder();
                taskQuery.setTables(DatabaseHelper.TABLE_TASK);
                taskQuery.setProjectionMap(DatabaseHelper.TASK_LIST_PROJECT_MAP);
                taskCursor = taskQuery.query(db, new String[] {Task.Column.ID}, Task.Column.CONTEXT_ID + " = ?",
                                new String[] {Integer.toString(contextId)}, null, null, null);
                if (taskCursor.moveToFirst()) {
                    int numberOfTasks = taskCursor.getCount() + numberOfTasksAdjustment;
                    ContentValues values = new ContentValues();
                    values.put(GtdContext.Column.NUMBER_OF_TASKS, numberOfTasks);
                    db.update(DatabaseHelper.TABLE_CONTEXT, values, GtdContext.ID + " = ?", new String[] {Integer.toString(contextId)});
                }
            } finally {
                if (taskCursor != null) {
                    taskCursor.close();
                }
            }
        }
    }
    
    /**
     * Update the number of tasks count of a context.
     * @param db Database.
     * @param taskId ID of task.
     */
    private void updateTaskNumberOfTasks(SQLiteDatabase db, SqlTemplate template, Integer taskId) {
        if (taskId != null) {
            Integer parentTaskId = taskId;
            parentTaskId = getTopParentTaskId(template, parentTaskId);
            updateNumberOfSubTasksOfParentTask(db, parentTaskId);
        }
    }

    /**
     * Update sub task count of parent and child tasks.
     * @param db Database.
     * @param parentTaskId ID of parent task.
     */
    private void updateNumberOfSubTasksOfParentTask(SQLiteDatabase db, Integer parentTaskId) {
        Map<Integer, Integer> visitedTasks = new HashMap<Integer, Integer>(); 
        dbHelper.updateNumberOfSubTasksOfTask(db, parentTaskId, visitedTasks);
    }

    /**
     * Find the top most parent task.
     * @param db Database.
     * @param taskId ID of (possibly sub-) task.
     * @return ID of top most parent task. 
     */
    private Integer getTopParentTaskId(SqlTemplate template, Integer taskId) {
        Integer parentTaskId = taskId;
        boolean foundParent = false;
        while(!foundParent) {
            Integer foundTaskId = getParentTaskId(template, parentTaskId);
            if (foundTaskId == null) {
                foundParent = true;
            } else {
                parentTaskId = foundTaskId;
            }
        }
        return parentTaskId;
    }

    /**
     * Get parent task ID of task.
     * @param taskId ID of task.
     * @param template SQL template.
     * @return Retrieved parent task ID, null if not present.
     */
    private Integer getParentTaskId(SqlTemplate template, Integer taskId) {
        Integer foundTaskId = template.query(DatabaseHelper.TABLE_TASK, DatabaseHelper.TASK_LIST_PROJECT_MAP,
                        new String[] {Task.Column.PARENT_TASK_ID},
                        Task.Column.ID + " = ?", new String[] {taskId.toString()}, 
                        new SqlTemplate.QueryHandler<Integer>() {
            @Override
            public Integer execute(Cursor cursor) {
                Integer taskId = null;
                if (!cursor.isNull(0)) {
                    taskId = cursor.getInt(0);
                }
                return taskId; 
            }
        });
        return foundTaskId;
    }
    
    /**
     * Delete sub tasks of task.
     * @param db Database.
     * @param taskId ID of task.
     * @return Number of deleted tasks.
     */
    private int deleteSubTasks(final SQLiteDatabase db, final ContentResolver contentResolver, Integer taskId) {
        int count = 0;
        SqlTemplate template = new SqlTemplate(db);
        template.query(DatabaseHelper.TABLE_TASK, DatabaseHelper.TASK_LIST_PROJECT_MAP,
                        new String[] {Task.Column.ID},
                        Task.Column.PARENT_TASK_ID + " = ?", new String[] {taskId.toString()}, 
                        new SqlTemplate.QueryHandler<Integer>() {
            @Override
            public Integer execute(Cursor cursor) {
                int count = 0;
                while(!cursor.isAfterLast()) {
                    int subTaskId = cursor.getInt(0);
                    count += deleteSubTasks(db, contentResolver, subTaskId);
                    changeContextTask(db, subTaskId, -1);
                    Uri itemUri = ContentUris.withAppendedId(Task.CONTENT_URI, subTaskId);
                    contentResolver.notifyChange(itemUri, null);
                    count += db.delete(DatabaseHelper.TABLE_TASK, Task.Column.ID + " = ?",
                                    new String[] {Integer.toString(subTaskId)});
                    cursor.moveToNext();
                }
                return count; 
            }
        });
        return count;
    }
    
    /**
     * Update the number of tasks count of a context.
     * @param db Database.
     * @param contextId ID of context.
     * @param numberOfTasksChange Change in number of tasks.
     */
    private void changeContextTask(SQLiteDatabase db, Integer taskId, int numberOfTasksChange) {
        if (taskId != null) {
            Cursor taskCursor = null;
            Cursor contextCursor = null;
            try {
                SQLiteQueryBuilder taskQuery = new SQLiteQueryBuilder();
                taskQuery.setTables(DatabaseHelper.TABLE_TASK);
                taskQuery.setProjectionMap(DatabaseHelper.TASK_LIST_PROJECT_MAP);
                taskCursor = taskQuery.query(db, new String[] {Task.Column.CONTEXT_ID}, Task.Column.ID + " = ?",
                                new String[] {Integer.toString(taskId)}, null, null, null);
                if (taskCursor.moveToFirst()) {
                    Integer contextId = taskCursor.getInt(0);
                    SQLiteQueryBuilder contextQuery = new SQLiteQueryBuilder();
                    contextQuery.setTables(DatabaseHelper.TABLE_CONTEXT);
                    contextQuery.setProjectionMap(DatabaseHelper.CONTEXT_LIST_PROJECT_MAP);
                    contextCursor = contextQuery.query(db, new String[] {GtdContext.Column.NUMBER_OF_TASKS},
                                    GtdContext.ID + " = ?", new String[] {Integer.toString(contextId)}, null, null, null);
                    if (contextCursor.moveToFirst()) {
                        if (contextCursor.isNull(0)) {
                            updateContextNumberOfTasks(db, contextId, numberOfTasksChange);
                        } else {
                            int numberOfTasks = 0;
                            numberOfTasks = contextCursor.getInt(0) + numberOfTasksChange;
                            if (numberOfTasks < 0) {
                                updateContextNumberOfTasks(db, contextId, numberOfTasksChange);
                            } else {
                                ContentValues values = new ContentValues();
                                values.put(GtdContext.Column.NUMBER_OF_TASKS, numberOfTasks);
                                db.update(DatabaseHelper.TABLE_CONTEXT, values, GtdContext.ID + " = ?",
                                                new String[] {Integer.toString(contextId)});
                            }
                        }
                    }
                }
            } finally {
                if (taskCursor != null) {
                    taskCursor.close();
                }
            }
        }
    }
    
    static {
        URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, "task", TASKS);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, "task/#", TASK_ID);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, "taskAndSubTasks/#", TASK_AND_SUB_TASKS);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, "task/completed", TASKS_COMPLETED);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, "inboxTasks", INBOX_TASKS);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, "topTasks", TOP_TASKS);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, "taskTimer/#", TASK_TIMER);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, "taskContacts/#", TASK_CONTACTS);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, "context", CONTEXTS);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, "context/#", CONTEXT_ID);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, "contextTasks", CONTEXT_TASKS);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, "project", PROJECTS);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, "taskAttachment", TASK_ATTACHMENT);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, "taskAttachmentsByTask/#", TASK_ATTACHMENTS_BY_TASK);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, BackupFile.URI_SUFFIX, BACKUP_FILE);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, BackupFile.URI_SUFFIX + "/#", BACKUP_FILE_ITEM);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, BackupFileSummary.URI_SUFFIX, BACKUP_SUMMARY);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, BackupFileSummary.URI_SUFFIX + "/#", BACKUP_SUMMARY_ITEM);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, BackupFile.URI_EXTERNAL_SUFFIX, BACKUP_FILE_EXTERNAL);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, BackupFile.URI_EXTERNAL_SUFFIX + "/#", BACKUP_FILE_EXTERNAL_ITEM);
        URI_MATCHER.addURI(GtdAndroid.PACKAGE, "help/*", HELP_PATH);

//        cUriMatcher.addURI(GtdAndroid.PACKAGE, "projects", PROJECTS);
//        cUriMatcher.addURI(GtdAndroid.PACKAGE, "projects/#", PROJECT_ID);
//        cUriMatcher.addURI(GtdAndroid.PACKAGE, "projectTasks", PROJECT_TASKS);
        
        sTaskContactListProjectMap = new HashMap<String, String>();
        sTaskContactListProjectMap.put(TaskContact._ID, DatabaseHelper.TABLE_TASK_CONTACT + "._id");
        sTaskContactListProjectMap.put(TaskContact.TASK_ID, DatabaseHelper.TABLE_TASK_CONTACT + ".taskId");
        sTaskContactListProjectMap.put(TaskContact.CONTACT_ID, DatabaseHelper.TABLE_TASK_CONTACT + ".contactId");
        
        
//        sProjectListProjectMap = new HashMap<String, String>();
//        sProjectListProjectMap.put(Project._ID, cProjectTableName + "._id");
//        sProjectListProjectMap.put(Project.NAME, cProjectTableName + ".name");
//        sProjectListProjectMap.put(Project.DEFAULT_CONTEXT_ID, cProjectTableName + ".defaultContextId");
//        sProjectListProjectMap.put(Project.ARCHIVED, cProjectTableName + ".archived");
    }
}
